[Java][Spring Boot] @Transactionalでトランザクション処理してロールバックする。
はじめに
データベースに対して日常的に行っている処理で、複数のクエリを実行し、どこかでエラーが発生したらなかった事にしたいという事が有ります。 トランザクションを使ってロールバックするという事ですね。 これはSpringの@Transactionalをアノテートする事で実現できるという事で試してみました。
環境
Mac OSX 10.10.5 Yosemite Java 1.8.0_91 Spring Boot 1.3.7 PostgreSQL 9.5.1 Eclipse Mars
テーブル
CREATE TABLE public.fruit ( id VARCHAR(2) NOT NULL, name VARCHAR(10), price integer, PRIMARY KEY(id) ); INSERT INTO fruit VALUES ('1','apple',300), ('2','orange',200), ('3','banana',100), ('4','cherry',50), ('5','pineapple',500), ('6','melon',800), ('7','watermelon', 600), ('8','strawberry',450);
postgres=# select * from fruit ; id | name | price ----+------------+------- 1 | apple | 300 2 | orange | 200 3 | banana | 100 4 | cherry | 50 5 | pineapple | 500 6 | melon | 800 7 | watermelon | 600 8 | strawberry | 450 (8 rows)
コード
プロパティファイルと依存関係は割愛します。
エンティティ
package com.transaction.sql; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import lombok.Data; import lombok.ToString; @Entity @Data @Table(name="fruit") public class Fruit { @Id private String id; private String name; private Integer price; }
lombokの@Dataを使用し、ToString、Getter、Setterを設定。
DAOインターフェース
package com.transaction.sql; import java.io.Serializable; public interface DaoInterface <T> extends Serializable { void getAll(); void update(String setColumn, String val, String whereColumn, String num1, String num2); void delete(String attributeName, String value); void insert(String column, String val1, Integer val2); }
名前通りの処理を行うメソッドを用意。
DAO
package com.transaction.sql; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.Query; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaDelete; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.CriteriaUpdate; import javax.persistence.criteria.Root; import org.springframework.transaction.annotation.Transactional; public class DaoObject implements DaoInterface<Fruit> { private EntityManager entityManager; private CriteriaBuilder builder; public DaoObject(EntityManager entityManager) { super(); this.entityManager = entityManager; this.builder = this.entityManager.getCriteriaBuilder(); } @Override public void getAll() throws RuntimeException { CriteriaQuery<Fruit> query = builder.createQuery(Fruit.class); Root<Fruit> root = query.from(Fruit.class); query.select(root); List<Fruit> list = entityManager.createQuery(query).getResultList(); System.out.println(list.toString()); } @Override public void update(String col1, String val, String col2, String start, String end) throws RuntimeException { CriteriaUpdate<Fruit> update = builder.createCriteriaUpdate(Fruit.class); Root root = update.from(Fruit.class); update.set(col1, val); update.where(builder.between(root.get(col2), start, end)); int result = this.entityManager.createQuery(update).executeUpdate(); System.out.println("UPDATE = " + result); } @Override public void delete(String column, String val) throws RuntimeException { CriteriaDelete<Fruit> delete = builder.createCriteriaDelete(Fruit.class); Root root = delete.from(Fruit.class); delete.where(builder.equal(root.get(column), val)); Query query = entityManager.createQuery(delete); query.executeUpdate(); } @Override public void insert(String column, String val1, Integer val2) throws RuntimeException { Fruit fruit = new Fruit(); fruit.setId(column); fruit.setName(val1); fruit.setPrice(val2); entityManager.persist(fruit); } }
27〜34行目、getAll()。全件取得するメソッド。確認用として一応用意しました。 発行されるネイティブクエリは下記。 SELECT * FROM テーブル名;
36〜44行目、update()。対象レコードを更新します。 CriteriaUpdateを使用する事以外はgetAll()とほぼ同じですね。 発行されるネイティブクエリは下記。 UPDATE テーブル SET カラム1 = 値 WHERE カラム2 BETWEEN 値1 AND 値2;
46〜53行目、delete()。対象レコードを削除します。 こちらはCriteriaDeleteを使用します。 発行されるネイティブクエリは下記。 DELETE テーブル名 WHERE カラム名 = 値;
55〜62行目、insert()。レコードを挿入します。 CriteriaではCriteriaInsertは用意されていない様なので、エンティティに値をセットしたものを作成してEntityManagerに投げています。 発行されるネイティブクエリは下記。 INSERT INTO テーブル名 (id, name, price) VALUES (値1, 値2, 値3);
トランザクションするクラス
package com.transaction.sql; import javax.annotation.PostConstruct; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Component public class TransactionTest { @PersistenceContext private EntityManager entityManager; private DaoObject dao; @PostConstruct public void init() { dao = new DaoObject(entityManager); } @Transactional public void execute() { try { dao.update("name", "AAA", "id", "3", "5"); dao.delete("id", "5"); dao.insert("9", "blossom", 1000); //throw new RuntimeException(); } catch (RuntimeException e) { e.printStackTrace(); } } }
17〜20行目、初期化処理。
22〜35行目、クエリを実行するメソッド。
22行目、@Transactionを付けているだけです。 ただし、ロールバックを有効にするには、このメソッドで例外を投げる必要が有ります。 メソッド内のメソッドで例外をtry-catchしても無効となるので注意。 詳細は下部の「ひっかかったところ」で紹介します。
30行目のコメントアウトを外して実行するとRuntimeExceptionが投げられ、ロールバックしてなかった事にしてくれます。
起動クラス
package com.transaction.sql; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication public class TransactionApplication { public static void main(String[] args) { try (ConfigurableApplicationContext cac = SpringApplication.run(TransactionApplication.class, args)) { TransactionTest app = cac.getBean(TransactionTest.class); app.execute(); }; } }
DIコンテナで管理されているTransactionTestクラスのBeanを取得してexecute()を実行しているだけです。
ひっかかったところ
@Transactionalをアノテートすると対象のメソッドで発行したクエリのトランザクション処理を行ってくれますが、ロールバックを実現するには必ずアノテートしたメソッドでExceptionを投げる必要が有る様です。 最初、アノテートしたからなんでもトランザクションで良しなにしてくれるんだろうと思って下記の様なコードを書いていましたが、ロールバックは実行されないのでした。
ダメなコード例
@Override public void update(String col1, String val, String col2, String start, String end) { try { CriteriaUpdate<Fruit> update = builder.createCriteriaUpdate(Fruit.class); Root root = update.from(Fruit.class); update.set(col1, val); update.where(builder.between(root.get(col2), start, end)); this.entityManager.createQuery(update).executeUpdate(); } catch (RuntimeException e) { } }
@Transactional public void execute() { dao.update("name","AAA","id" ,"1","2"); dao.update("name","BBB","ids","1","2"); }
2行目、このUPDATEは成功します。 3行目、こっちは失敗します。where句のカラム名を設定する箇所で"ids"という存在しないカラム指定しているためです。 しかし2行目のUPDATEがRollbackされずにテーブルを更新してしまいます。
さいごに
フレームワークを使用しなくてもロールバックは実現できますが、アノテーションだけで済むとだいぶすっきりしますね。